package au.edu.adelaide.mips_soft_core.board_interface_pc.core;

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.Semaphore;

import au.edu.adelaide.adept_sdk_java.AdeptSDKJava;
import au.edu.adelaide.adept_sdk_java.AdeptSDKJava.DeppNotEnabledException;
import au.edu.adelaide.adept_sdk_java.AdeptSDKJava.HIF;
import au.edu.adelaide.adept_sdk_java.AdeptSDKJava.HIFDisposedException;


public class Device {

	private static final byte DEFAULT_ADDRESS = 0;
	static final byte OPERATION_GET_BOARD_INFO = 1;
	static final byte OPERATION_DOWNLOAD_TO_BOARD_MEM = 2;
	static final byte OPERATION_DOWNLOAD_FROM_BOARD_MEM = 3;
	static final byte OPERATION_BOARD_CONTROL = 4;
	static final byte OPERATION_DEBUG1 = 5;
	//private static final byte OPERATION_DEBUG = 14;

	private HIF hif;

	private byte[] cpuFrequencies = null;
	private int[] memorySize = null;
	
	int[] pcs = new int[1];
	
	String name;
	String connectionString;
	
	public static final Device NO_DEVICE = new Device("No Device Connected", null);
	
	

	Random ran = new Random(System.currentTimeMillis());
	Status status = Status.DISCONNECTED;
	
	ArrayList<DeviceListener> listeners = new ArrayList<DeviceListener>();
	
	Semaphore accessLock = new Semaphore(1);
	
	ArrayList<Processor> processors = new ArrayList<Processor>();
	
	BoardMode boardMode = BoardMode.DOWNLOAD_MODE;
	
	Device(String name, String connectionString){
		this.name = name;
		this.connectionString = connectionString;
	}

	public static enum Status{
		DISCONNECTED,
		CONNECTING,
		CONNECTED,
		READY
	}
	Thread task;

	public void connect() {
		this.task = new Thread(){
			
			public void run(){
				connectBlocking();
			}
			
		};
		this.task.start();
		
	}
	
	public void disconnect(){
		disconnectBlocking(true);
	}	
	
	public void connectBlocking(){
		accessLock.acquireUninterruptibly();
		if(status != Status.DISCONNECTED){
			tellListenersError("Device was not in disconnected state");
			accessLock.release();
			return;
		}else{
			status = Status.CONNECTING;
			try{
				hif = AdeptSDKJava.getInstance().createHIFHandle();
				if(!AdeptSDKJava.getInstance().jDmgrOpen(hif, name)){
					Core.getCore().logger.write("Failed to open device", Core.LOG_LEVEL_ERROR);
					tellListenersError("Adept runtime failed to open device");
					return;
				}
				if(!AdeptSDKJava.getInstance().jDeppEnable(hif)){
					Core.getCore().logger.write("Failed to open device", Core.LOG_LEVEL_ERROR);
					tellListenersError("Adept failed to enable DEPP protocol with device");
					return;
				}
				status = Status.CONNECTED;
				Core.getCore().deviceConnected(Device.this);
			}catch(Exception ex){
				tellListenersError(ex.getMessage());
				ex.printStackTrace();
			}finally{
				accessLock.release();
			}
		}
	}
	
	/**
	 * This is a blocking function
	 * notify is in reference to if listeners will be notified
	 */
	public void disconnectBlocking(boolean notify){
		if(status == Status.DISCONNECTED){
			if(notify){
				tellListenersError("device is already disconnected");
			}
			return;
		}
		try {
			AdeptSDKJava.getInstance().jDeppDisable(hif);
		} catch (HIFDisposedException | DeppNotEnabledException e) {
			e.printStackTrace();
		}
		try{
			AdeptSDKJava.getInstance().jDmgrClose(hif);
		}catch(Exception ex){
			ex.printStackTrace();
		}
		hif.dispose();
		hif = null;
		this.status = Status.DISCONNECTED;
		if(notify){
			Core.getCore().deviceDisconnected(this);
		}
		
	}
	
	
	private void tellListenersError(String message){
		for(int i = 0; i < listeners.size(); i++){
			listeners.get(i).error(message);
		}
	}
	
	public int getNumberOfProcessor(){
		return this.processors.size();
	}
	
	public Processor getProcessor(int index){
		return this.processors.get(index);
	}
	
	
	public void addDeviceListener(DeviceListener listener){
		this.listeners.add(listener);
	}
	
	
	public void removeListener(DeviceListener listener){
		this.listeners.remove(listener);
	}

	

	private byte[] genOperation(byte operation){
		byte[] out = new byte[2];
		byte[] ranB = new byte[1];
		ran.nextBytes(ranB);
		ranB[0] = (byte)(2  & 0x0F);
		out[0] = (byte)((operation & 0x0F) | ((ranB[0] << 4) & 0xF0));
		out[1] = (byte)(((operation << 4) & 0xF0) | (~ranB[0] & 0x0F));
		return out;
	}
	
	private static final byte GET_PC = 2;
	private static final byte GET_REGISTER_AND_PC = 3;
	
	private void debugGetPC(byte processor){
		try{
			if(!doOperation(OPERATION_DEBUG1, "DEBUG1"))
				return;
			if(!doOperation(GET_PC, "GETTING PC"))
				return;
			if(!doOperation(processor, "Couldn't select processor"))
				return;
			int[] results = new int[1];
			if(!this.getIntS(results)){
				System.out.println("couldn't get PC of processor");
				return;
			}
			this.pcs[0] = results[0];
			byte[] finishByte = new byte[1];
			if(!this.getByte(finishByte)){
				System.out.println("couldn't get finish byte");
				return;
			}
			byte acceptanceByte = (byte)0xAF;
			if(finishByte[0] != acceptanceByte){
				System.out.println("failed to get correct finish byte");
			}
		}catch(Exception ex){
			ex.printStackTrace();
		}
		
		
	}
	
	// this requests the board to a step then return all registers
	private void debugGetState(byte processor) {
		try{
		if(!doOperation(OPERATION_DEBUG1, "DEBUG1"))
			return;
		if(!doOperation(GET_REGISTER_AND_PC, "GETTING PC"))
			return;
		if(!doOperation(processor, "Couldn't select processor"))
			return;
		int[] registers = new int[32];
		if(!this.getIntS(registers)){
			System.out.println("couldn't get registers of the processor");
			return;
		}
		
		
		int[] results = new int[1];
		if(!this.getIntS(results)){
			System.out.println("couldn't get PC of processor");
			return;
		}
		this.pcs[0] = results[0];
		byte[] finishByte = new byte[1];
		if(!this.getByte(finishByte)){
			System.out.println("couldn't get finish byte");
			return;
		}
		byte acceptanceByte = (byte)0xAF;
		if(finishByte[0] != acceptanceByte){
			System.out.println("failed to get correct finish byte");
		}
		
		for(int i = 0; i < registers.length; i++){
			System.out.println("Register " + i + " :0x" + Integer.toHexString(registers[i]));
		}
		}catch(Exception ex){
			ex.printStackTrace();
		}
		
	}
	

	
	public boolean doOperation(byte operation, String operationName) throws Exception{
		byte[] operationD = genOperation(operation);
		byte[] input = new byte[1];
		boolean r1 = putByte(operationD[0]);
		if(r1 == false){
			Core.getCore().logger.write("Failed to write operation to board, get FPGA config, and try reset", Core.LOG_LEVEL_ERROR);
			return false;
		}
		boolean r2 = getByte(input);
		if(!r2){
			Core.getCore().logger.write(operationName+ ": Communication error GET", Core.LOG_LEVEL_ERROR);
			return false;
		}
		if(input[0] != operationD[1]){
			Core.getCore().logger.write("Operation" + operationName + "not accept by bored", Core.LOG_LEVEL_ERROR);
			return false;
		}
		return true;
	}
	
	public static enum BoardMode{
		DOWNLOAD_MODE("PC Downloader", (byte)0),
		BOARD_MEMORY_VIEWER("Board Memory Viewer", (byte)1),
		DEBUGER("Debugger", (byte)2),
		RUN("Run", (byte)3);
		public String name;
		public byte number;
		BoardMode(String name, byte number){
			this.name = name;
			this.number = number;
		}
		
		public String toString(){
			return this.name;
		}
	}
	
	public void changeBoardMode(final BoardMode mode){
		new Thread(){

			public void  run(){
				changeBoardModeBlocking(mode);
			}}.start();
	}
	
	public static final byte CHANGE_MODE = 1;
	
	public boolean changeBoardModeBlocking(BoardMode mode){
		try{
			this.accessLock.acquireUninterruptibly();
			if(!doOperation(OPERATION_BOARD_CONTROL, "Board Control"))
				return false;
			if(!doOperation(CHANGE_MODE, "Change Mode"))
				return false;
			//switching of modes is supported on this board
			if(!putByte(mode.number)){
				Core.getCore().logger.write("Board Control:Change Mode: " + "Communication error PUT", Core.LOG_LEVEL_ERROR);
				return false;
			}
			byte[] resultD = new byte[1];
			if(!getByte(resultD)){
				Core.getCore().logger.write("Board Control:Change Mode: " + "Communication error GET", Core.LOG_LEVEL_ERROR);
				return false;
			}
			byte acceptanceByte = (byte)0xAF;
			if(resultD[0] != acceptanceByte){
				Core.getCore().logger.write("Board Control:Change Mode: " + "Board declined mode change", Core.LOG_LEVEL_ERROR);
				return false;
			}
			return true;
		}catch(Exception ex){
			ex.printStackTrace();
		}finally{
			this.accessLock.release();
		}
		return false;
	}
	
	public void downloadInfo(){
		try{
			new Thread(){
				
				public void run(){
					try{
						boolean worked = downloadInfoBlocking();
						if(!worked){
							disconnectBlocking(true);
							return;
						}
						for(int i = 0; i < listeners.size(); i++){
							listeners.get(i).boardDiagnosed();
						}
					}catch(Exception ex){
						ex.printStackTrace();
					}
				}
				
			}.start();
		}catch(Exception ex){
			ex.printStackTrace();
		}
	}
	


	public boolean downloadInfoBlocking() {
		try{
			accessLock.acquireUninterruptibly();
			byte[] operationD = genOperation(OPERATION_GET_BOARD_INFO);
			byte[] data = new byte[1];
			boolean result = putByte(operationD[0]);
			if(result == false){
				Core.getCore().logger.write("Failed to write operation to board. re-program FPGA with board config, and try reset", Core.LOG_LEVEL_ERROR);
				this.disconnectBlocking(true);
				return false;
			}

			result = getByte(data);
			if(!result || data[0] != operationD[1]){
				Core.getCore().logger.write("Failed to get FPGA config, and try reset", Core.LOG_LEVEL_ERROR);
				System.out.println("operation not accepted");
				return false;
				
			}
			result = getByte(data);
			if(!result){
				System.out.println("failed");
			}
			this.cpuFrequencies = new byte[(data[0] >> 4) & 0x0F];
			this.memorySize = new int[data[0] & 0x0F];
			this.getBytes(this.cpuFrequencies);
			this.getIntS(this.memorySize);
			this.processors.clear();
			for(byte i = 0; i < this.cpuFrequencies.length; i++){
				this.processors.add(new Processor(this, i));
				
			}
			return true;
		}catch(Exception ex){
			ex.printStackTrace();
		}finally{
			accessLock.release();
		}
		return false;
			
	}
	
	public boolean downloadFromBoardMemory(int memoryModule, int byteOffset, int numberOfBytes, OutputStream out){
		byte[] buffer = new byte[4];
		try{
			byte[] operationD = genOperation(OPERATION_DOWNLOAD_FROM_BOARD_MEM);
			byte[] input = new byte[1];
			byte[] input2 = new byte[1];
			boolean r1 = putByte(operationD[0]);
			boolean r2 = this.getByte(input);
			boolean r3 = this.putByte((byte)memoryModule);
			boolean r4 = this.putInt(byteOffset / 4);
			boolean r5 = this.putInt(numberOfBytes/4);
			boolean r6 = this.getByte(input2);
			System.out.println("download memory from board " + r1 + " " + r2 + " " + r3 + " " + r4 + " " + r5 + " " + r6);
			System.out.println("download memory from board " + input[0] + " " + input2[0]);
			while(numberOfBytes != 0){
				int amount  = 4; //numberOfBytes > 4 ? 4 : numberOfBytes;
				numberOfBytes -= amount;
				boolean r7 = this.getBytes(buffer, 4);
				
				byte b = buffer[0];
				buffer[0] = buffer[3];
				buffer[3] = b;
				b = buffer[1];
				buffer[1] = buffer[2];
				buffer[2] = b;
				//System.out.println("download mem " + r7);
				out.write(buffer, 0, amount);
			}
			byte acceptanceByte = (byte)0xAF;
			boolean r8 = this.putByte(acceptanceByte);
			System.out.println("download mem uploaded last byte " + r8);
			return true;
		}catch(Exception ex){
			ex.printStackTrace();
			return false;
		}
	}
	
	public boolean uploadToBoardMemory(int memoryModule, byte[] data){
		try{
			byte[] operationD = genOperation(OPERATION_DOWNLOAD_TO_BOARD_MEM);
			byte[] input3 = new byte[1];
			boolean result = putByte(operationD[0]);
			if(!result){
				return false;
			}
			boolean res6 = this.getByte(input3);
			boolean res1 = this.putByte((byte)memoryModule);
			boolean res2 = this.putInt(data.length / 4);
			byte[] input = new byte[1];
			boolean res3 = this.getByte(input);
			if(!res6 | !res1 | !res2 | !res3){
				return false;
			}
			for(int i = 0; i < data.length; i++){
				boolean res4 = this.putByte(data[i]);
			}
			byte[] input2 = new byte[1];
			boolean res5 = this.getByte(input2);
			return true;
		}catch(Exception ex){
			ex.printStackTrace();
			return false;
		}
		
	}

	
	
	public int getNumberOfCPUModules(){
		if(this.cpuFrequencies == null){
			return -1;
		}
		return this.cpuFrequencies.length;
	}
	
	public int getFrequencyOfCPUModule(byte cpuModuleNumber){
		return this.cpuFrequencies[cpuModuleNumber];
	}
	
	public static int lookUpFrequencyTableValue(byte tableIndex){
		switch(tableIndex){
		case 1: return 50000;
		case 2: return 60000;
		case 3: return 70000;
		case 4: return 80000;
		default: return 0;
		}
	}

	public int getNumberOfMemoryModules(){
		if(this.memorySize == null){
			return -1;
		}
		return this.memorySize.length;
	}

	public int getSizeOfMemoryModule(byte memoryModuleNumber){
		return this.memorySize[memoryModuleNumber];
	}

	public String toString(){
		return this.name;
	}
	
	boolean putByte(byte b) throws HIFDisposedException, DeppNotEnabledException{
		return AdeptSDKJava.getInstance().jDeppPutReg(hif, DEFAULT_ADDRESS, b, false);
	}

	boolean getByte(byte[] data) throws HIFDisposedException, DeppNotEnabledException {
		return AdeptSDKJava.getInstance().jDeppGetReg(hif, DEFAULT_ADDRESS, data, false);
	}
	
	private boolean getBytes(byte[] data) throws HIFDisposedException, DeppNotEnabledException{
		return getBytes(data, data.length);
	}
	
	private boolean getBytes(byte[] data, int number) throws HIFDisposedException, DeppNotEnabledException{
		return AdeptSDKJava.getInstance().jDeppGetRegRepeat(hif, DEFAULT_ADDRESS, data, number, false);
	}
	
	private boolean putInt(int word) throws HIFDisposedException, DeppNotEnabledException {
		boolean res = AdeptSDKJava.getInstance().jDeppPutReg(hif, DEFAULT_ADDRESS, (byte)((word >> 24) & 0xFF), false);
		if(!res){
			return false;
		}
		res = AdeptSDKJava.getInstance().jDeppPutReg(hif, DEFAULT_ADDRESS, (byte)((word >> 16) & 0xFF), false);
		if(!res){
			return false;
		}
		res = AdeptSDKJava.getInstance().jDeppPutReg(hif, DEFAULT_ADDRESS, (byte)((word >> 8) & 0xFF), false);
		if(!res){
			return false;
		}
		return AdeptSDKJava.getInstance().jDeppPutReg(hif, DEFAULT_ADDRESS, (byte)(word & 0xFF), false);
	}
	
	boolean getIntS(int[] data) throws HIFDisposedException, DeppNotEnabledException{
		byte[] dataIn = new byte[data.length * 4];
		boolean res = AdeptSDKJava.getInstance().jDeppGetRegRepeat(hif, DEFAULT_ADDRESS, dataIn, dataIn.length, false);
		for(int i = 0; i < data.length; i++){
			data[i] = ((dataIn[i * 4] << 24) & 0xFF000000) | ((dataIn[(i*4) + 1] << 16) & 0x00FF0000) | ((dataIn[(i*4) + 2] << 8) & 0x0000FF00) | ((dataIn[(i*4) + 3]) & 0x000000FF);
		}
		return res;
	}

}
